package org.warehouse.dao;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Date;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.warehouse.dao.annotation.Table;
import org.warehouse.dao.engine.DataObject;
import org.warehouse.dao.engine.IDbExecutor;
import org.warehouse.dao.exception.DaoException;
import org.warehouse.entity.IEntity;

import com.google.common.collect.ImmutableList;

/**
 * @author atma
 * 
 * @param <T>
 */
public abstract class AbstractDao<T extends IEntity> implements IDao<T> {

    public final Logger logger = LoggerFactory.getLogger(getClass());

    private final Class<T> entityType;

    private final String tableName;

    private final AtomicInteger index;

    @Autowired
    private IDbExecutor dbExecutor;

    public AbstractDao(Class<T> entityType) {
        this.entityType = entityType;

        Table annotation = getEntityType().getAnnotation(Table.class);
        if (annotation == null) {
            tableName = getEntityType().getSimpleName();
        } else {
            tableName = annotation.value();
        }

        index = new AtomicInteger();
    }

    @PostConstruct
    @SuppressWarnings("unused")
    private void initMaxIndex() {
        index.set(getMaxIndex());
    }

    protected final Logger getLogger() {
        return logger;
    }

    protected final Class<T> getEntityType() {
        return entityType;
    }

    public final String getTableName() {
        return tableName;
    }
    
    public String getIdColumnName() {
        return "ID";
    }

    protected T getEntityInstance() {
        try {
            return getEntityType().getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new DaoException(e);
        }
    }

    public T createAndFillEntity(SqlRowSet rowSet) {
        T entity = getEntityInstance();
        try {
            for (String columnName : rowSet.getMetaData().getColumnNames()) {
                String methodName = prepareMethodName("set", columnName.toLowerCase());
                findMethod(methodName).invoke(entity, rowSet.getObject(columnName));
            }
            return entity;
        } catch (Exception e) {
            throw new DaoException(e);
        }
    }
    
    protected Method findMethod(String methodName) {
        List<Class<?>> classes = ImmutableList.<Class<?>>of(Integer.class, String.class, Date.class);
        for (Class<?> clazz : classes) {
            try {
                return getEntityType().getMethod(methodName, clazz);
            } catch (Exception e) {
            }
        }
        throw new DaoException("Couldn't find " + methodName
                + ". Probably them has got unsupported arguments. Engine supports only: Integer, String, Date");
    }

    private Integer getMaxIndex() {
        String request = MessageFormat.format("SELECT MAX({0}) FROM {1}", getIdColumnName(),getTableName());
        SqlRowSet set = dbExecutor.invoke(request);
        set.next();
        return set.getInt( MessageFormat.format("MAX({0})", getIdColumnName()));
    }

    @Override
    public List<T> getAll() {
        String request = "SELECT * FROM " + getTableName();
        SqlRowSet rowSet = dbExecutor.invoke(request);

        List<T> entities = new ArrayList<T>();
        while (rowSet.next()) {
            entities.add(createAndFillEntity(rowSet));
        }

        return entities;
    }

    @Override
    public T get(Integer id) throws DaoException {
        String request = MessageFormat.format("SELECT * FROM {0} WHERE {1} = ?", getTableName(), getIdColumnName());
        SqlRowSet rowSet = dbExecutor.invoke(request, ImmutableList.<DataObject<?>> of(DataObject.of(id)));

        if (!rowSet.next()) {
            return null;
        }
        
        return createAndFillEntity(rowSet);
    }

    @Override
    public void delete(Integer id) throws DaoException {
        String request = MessageFormat.format("DELETE FROM {0} WHERE {1} = ?", getTableName(), getIdColumnName());
        dbExecutor.invokeUpdate(request, ImmutableList.<DataObject<?>> of(DataObject.of(id)));
    }

    @Override
    public Integer put(T entity) throws DaoException {
        Field[] fields = validation(entity);

        StringBuilder request = new StringBuilder("INSERT INTO " + getTableName() + " VALUES(");

        List<DataObject<?>> dataList = new ArrayList<DataObject<?>>();

        Integer id = index.addAndGet(1);

        dataList.add(DataObject.of(id));

        for (Field field : fields) {
            request.append("?,");

            if (getIdColumnName().equalsIgnoreCase(field.getName())) {
                continue;
            }

            addValueToList(dataList, entity, field.getName());
        }

        request.deleteCharAt(request.length() - 1);
        request.append(")");

        dbExecutor.invokeUpdate(request.toString(), dataList);

        return id;
    }

    @Override
    public void update(Integer id, T entity) throws DaoException {
        Field[] fields = validation(entity);

        StringBuilder request = new StringBuilder("UPDATE " + getTableName() + " SET ");

        List<DataObject<?>> dataList = new ArrayList<DataObject<?>>();

        for (Field field : fields) {
            if (getIdColumnName().equalsIgnoreCase(field.getName())) {
                continue;
            }

            request.append(field.getName() + " = ?,");
            addValueToList(dataList, entity, field.getName());
        }

        request.deleteCharAt(request.length() - 1);
        request.append(" WHERE id = ?");
        dataList.add(DataObject.of(id));

        dbExecutor.invokeUpdate(request.toString(), dataList);

    }

    private Field[] validation(T entity) {
        Field[] fields = entity.getClass().getDeclaredFields();
        if (fields.length == 0) {
            throw new DaoException("Entity " + getEntityType().getName() + "doesn't have any fields");
        }
        return fields;

    }

    private void addValueToList(List<DataObject<?>> dataList, T entity, String fieldName) {
        try {
            String methodName = prepareMethodName("get", fieldName);
            Method method = entity.getClass().getMethod(methodName);
            dataList.add(DataObject.of(method.invoke(entity)));
        } catch (Exception e) {
            throw new DaoException(e);
        }
    }

    private String prepareMethodName(String prefix, String fieldName) {
        return prefix + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1, fieldName.length());
    }

}
